{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ">原文地址 [segmentfault.com](https://segmentfault.com/a/1190000023923199)\n",
    "\n",
    "背景\n",
    "--\n",
    "\n",
    "本文主要在 python 代码的性能分析优化方面进行讨论，旨在解决一些语言层面比较常见的性能瓶颈，是在平时工作中的一些积累和总结，会比较基础和全面，顺便也会介绍一些在服务架构上的优化经验。\n",
    "\n",
    "### 一、Python 语言背景\n",
    "\n",
    "python 简单易学以及在数据计算分析方面优异的特点催生了庞大的用户群体和活跃社区性，使得它在数据分析、机器学习等领域有着先天的优势， 同时由于其协程特性和广泛的第三方支持，python 在在线服务上也有广泛的使用。但是 python 在性能问题上有所有动态解释型高级语言的通病，也是制约 python 进一步广泛应用的重要因素。这也是这类解释型脚本语言的通病：\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMwxD)\n",
    "\n",
    "### 二、业务架构背景\n",
    "\n",
    "单独脱离具体的业务应用场景来看性能问题是比较片面的。下面以我们当前的后端架构来看下 python 性能瓶颈在业务应用上的具体表现。该系统是基于大数据和机器学习模型的风在线风控系统，它为大量金融机构提供风控服务，基于大量结构化和非结构化数据、外部数据源、超万维的特征、以及复杂的建模技术。些也导致我们基于 python 的服务性能面临着严峻考验 。下图是架构简图：\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMwTk)\n",
    "\n",
    "性能分析\n",
    "----\n",
    "\n",
    "对代码优化的前提是需要了解性能瓶颈在什么地方，程序运行的主要时间是消耗在哪里，常见的可以在日志中打点来统计运行时间，对于比较复杂的代码也可以借助一些工具来定位，python 内置了丰富的性能分析工具，能够描述程序运行时候的性能，并提供各种统计帮助用户定位程序的性能瓶颈。常见的 profilers:cProfile,profile，line\\_profile,pprofile 以及 hotshot 等，当然一些 IDE 比如 pycharm 中也继承了完善的 profiling。这里我们只介绍有代表性的几种性能分析方法：\n",
    "\n",
    "### 一、利用装饰器实现函数耗时统计打点\n",
    "\n",
    "装饰器就是通过闭包来给原有函数增加新功能，python 可以用装饰器这种语法糖来给函数进行耗时统计，但是这仅限于一般的同步方法，**在协程中，更一般地说在生成器函数中**，因为 yield 会释放当前线程，耗时统计执行到 yield 处就会中断返回，导致统计的失效。  \n",
    "如下是一个包含两层闭包（因为要给装饰器传参）的装饰器：\n",
    "\n",
    "```\n",
    "def time\\_consumer(module\\_name='public\\_module'):\n",
    "\n",
    "    def time\\_cost(func):\n",
    "        \n",
    "        filepath =sys.\\_getframe(1).f\\_code.co\\_filename\n",
    "        @wraps(func)\n",
    "        def warpper(\\*args,\\*\\*kwargs):\n",
    "            t1 = time.time()\n",
    "            res = func(\\*args,\\*\\*kwargs)\n",
    "            t2 = time.time()\n",
    "            content={}\n",
    "            try:\n",
    "            \n",
    "                content\\['time\\_cost'\\] = round(float(t2-t1),3)\n",
    "                content\\['method'\\] = func.\\_\\_name\\_\\_\n",
    "                content\\['file'\\] = filepath.split(os.sep)\\[-1\\]\n",
    "                content\\['module'\\] = module\\_name\n",
    "            \n",
    "                content\\_res = json.dumps(content)\n",
    "                time\\_cost\\_logger.info(content\\_res)\n",
    "            \n",
    "            except Exception as e:\n",
    "                time\\_cost\\_logger.warning('%s detail: %s' % (str(e), traceback.format\\_exc()))\n",
    "            return res\n",
    "        return warpper\n",
    "    return time\\_cost\n",
    "\n",
    "```\n",
    "\n",
    "### 二、函数级性能分析工具 cprofile\n",
    "\n",
    "cProfile 自 python2.5 以来就是标准版 Python 解释器默认的性能分析器，它是一种确定性分析器，只测量 CPU 时间，并不关心内存消耗和其他与内存相关联的信息。\n",
    "\n",
    "#### 性能分析结果\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMw1J)\n",
    "\n",
    "#### 字段含义\n",
    "\n",
    "ncalls：函数被调用的次数。  \n",
    "tottime：函数内部消耗总时间。  \n",
    "percall：每次调用平均消耗时间。  \n",
    "cumtime：消费时间的累计和。  \n",
    "filename:lineno(function)：被分析函数所在文件名、行号、函数名。\n",
    "\n",
    "#### 使用方法\n",
    "\n",
    "1、针对单个文件的性能分析：\n",
    "\n",
    "```\n",
    " python -m cProfile -s tottime test.py\n",
    "\n",
    "```\n",
    "\n",
    "2、针对某个方法的性能分析：\n",
    "\n",
    "```\n",
    "import cProfile\n",
    "def function():\n",
    "    pass\n",
    "if \\_\\_name\\_\\_ == '\\_\\_main\\_\\_':\n",
    "    cProfile.run(\"function()\")\n",
    "\n",
    "```\n",
    "\n",
    "3、项目中针对实时服务的性能分析：\n",
    "\n",
    "```\n",
    " \n",
    "\n",
    " def \\_start\\_profile(self):\n",
    "     import cProfile\n",
    "     self.pr = cProfile.Profile()\n",
    "     self.pr.enable()\n",
    "     \n",
    " def \\_print\\_profile\\_result(self):\n",
    "     if not self.pr:\n",
    "         return\n",
    "     self.pr.disable()\n",
    "     import pstats\n",
    "     import StringIO\n",
    "     s = StringIO.StringIO()\n",
    "     stats = pstats.Stats(self.pr, stream=s).strip\\_dirs().sort\\_stats('tottime')\n",
    "     stats.print\\_stats(50)\n",
    "\n",
    "```\n",
    "\n",
    "### 三、行级分析工具 pprofile/line\\_profile\n",
    "\n",
    "使用 line\\_profile 需要引入\\_kernprof\\_\\_，因此我们这里选用 pprofile，虽然 pprofile 的效率没有 line\\_profile 高，但在做性能分析时这是可以忽略的。pprofile 的用法和 cprofile 的用法三完全一致。\n",
    "\n",
    "#### 性能分析结果\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMw6q)  \n",
    "![](https://segmentfault.com/img/bVbMw6v)\n",
    "\n",
    "#### 字段含义\n",
    "\n",
    "Line：行号  \n",
    "Hits：该行代码执行次数  \n",
    "Time：总执行耗时  \n",
    "Time per hit：单次执行耗时  \n",
    "%：耗时占比\n",
    "\n",
    "### 四、性能分析总结\n",
    "\n",
    "我们在做性能分析时，可以挑选任何方便易用的方法或工具进行分析。但总体的思路是由整体到具体的。例如可以通过 cprofile 寻找整个代码执行过程中的耗时较长的函数，然后再通过 pprofile 对这些函数进行逐行分析，最终将代码的性能瓶颈精确到行级。\n",
    "\n",
    "python 性能优化方法\n",
    "-------------\n",
    "\n",
    "### 一、优化思路\n",
    "\n",
    "Python 的性能优化方式有很多，可以从不同的角度出发考虑具体问题具体分析，但可以归结为两个思路：从服务架构和 CPU 效率层面，**将 CPU 密集型向 IO 密集型优化**。从代码执行和 cpu 利用率层面，**要提高代码性能以及多核利用率**。比如，基于此，python 在线服务的优化思路可以从这几方面考虑：\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMxa6)\n",
    "\n",
    "### 二、代码优化\n",
    "\n",
    "#### 使用字典 / 集合等 hash 等数据结构\n",
    "\n",
    "常用操作：检索、去重、交集、并集、差集  \n",
    "**1、在字典 / 集合中查找（以下代码中均省略记时部分）**\n",
    "\n",
    "```\n",
    "dic = {str(k):1 for k in xrange(1000000)}\n",
    "if 'qth' in dic.keys():\n",
    "    pass\n",
    "if 'qth' in dic:\n",
    "    pass\n",
    "\n",
    "\n",
    "```\n",
    "\n",
    "耗时：  \n",
    "0.0590000152588  \n",
    "0.0\n",
    "\n",
    "**2、使用集合求交集**\n",
    "\n",
    "```\n",
    "list1=list(range(10000))\n",
    "list2=\\[i\\*2 for i in list1\\]\n",
    "s1=set(list1)\n",
    "s2=set(list2)\n",
    "list3 = \\[\\]\n",
    "\n",
    "for k in list1:\n",
    "    if k in list2:\n",
    "        list3.append(k)\n",
    "\n",
    "s3 = s1&s2\n",
    "\n",
    "```\n",
    "\n",
    "耗时：  \n",
    "0.819999933243  \n",
    "0.001000165939\n",
    "\n",
    "**Ps：集合操作在去重、交并差集等方面性能突出：**\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMxh3)\n",
    "\n",
    "#### 使用生成器代替可迭代对象\n",
    "\n",
    "节省内存和计算资源，不需要计算整个可迭代对象，只计算需要循环的部分。  \n",
    "**1、使用 xrange 而不是 range（python3 中无区别）**\n",
    "\n",
    "```\n",
    "for i in range(1000000):\n",
    "    pass\n",
    "for i in xrange(1000000):\n",
    "    pass\n",
    "\n",
    "```\n",
    "\n",
    "耗时：  \n",
    "0.0829999446869  \n",
    "0.0320000648499\n",
    "\n",
    "**2、列表推导使用生成器**\n",
    "\n",
    "```\n",
    "dic = {str(k):1 for k in xrange(100000)}\n",
    "list1 = \\[k for k in dic.keys()\\]\n",
    "list1 = (k for k in dic.keys())\n",
    "\n",
    "```\n",
    "\n",
    "耗时：  \n",
    "0.0130000114441  \n",
    "0.00300002098083\n",
    "\n",
    "**3、复杂逻辑产生的迭代对象使用生成器函数来代替**\n",
    "\n",
    "```\n",
    "def fib(max):\n",
    "    n,a,b =0,0,1\n",
    "    list = \\[\\]\n",
    "    while n < max:\n",
    "        a,b =b,a+b\n",
    "        n = n+1\n",
    "        list.append(b)\n",
    "    return list\n",
    "\n",
    "for i in fib(100000):\n",
    "    pass\n",
    "def fib2(max):\n",
    "    n, a, b = 0, 0, 1\n",
    "    while n < max:\n",
    "        yield b\n",
    "        a, b = b, a + b\n",
    "        n = n + 1\n",
    "\n",
    "for  i in fib2(100000):\n",
    "    pass\n",
    "\n",
    "```\n",
    "\n",
    "耗时：  \n",
    "0.713000059128  \n",
    "0.138999938965\n",
    "\n",
    "#### 减少循环和冗余\n",
    "\n",
    "这部分比较容易理解就不再附上示例了。  \n",
    "i） 在循环中不要做和迭代对象无关的事。将无关代码提到循环上层。  \n",
    "ii） 使用列表解析和生成器表达式  \n",
    "iii） 对于 and，应该把满足条件少的放在前面，对于 or，把满足条件多的放在前面。  \n",
    "iv） 迭代器中的字符串操作：是有 join 不要使用 +。  \n",
    "v） 尽量减少嵌套循环，不要超过三层，否则考虑优化代码逻辑、优化数据格式、使用 dataframe 代替循环等方式。\n",
    "\n",
    "#### 尝试 pandas 和 numpy 中的数据结构\n",
    "\n",
    "一个 NumPy 数组基本上是由元数据（维数、形状、数据类型等）和实际数据构成。数据存储在一个均匀连续的内存块中，该内存在系统内存（随机存取存储器，或 RAM）的一个特定地址处，被称为数据缓冲区。这是和 list 等纯 Python 结构的主要区别，list 的元素在系统内存中是分散存储的。这是使 NumPy 数组如此高效的决定性因素。\n",
    "\n",
    "```\n",
    "import numpy as np\n",
    "def pySum(n):\n",
    "    a=list(range(n))\n",
    "    b=list(range(0,5\\*n,5))\n",
    "    c=\\[\\]\n",
    "    for i in range(len(a)):\n",
    "        c.append(a\\[i\\]\\*\\*2+b\\[i\\]\\*\\*3)\n",
    "    return c\n",
    "def npSum(n):\n",
    "    a=np.arange(n)\n",
    "    b=np.arange(0,5\\*n,5)\n",
    "    c=a\\*\\*2+b\\*\\*3\n",
    "    return c\n",
    "a=pySum(100000)\n",
    "b=npSum(100000)\n",
    "\n",
    "```\n",
    "\n",
    "耗时：  \n",
    "0.138999891281  \n",
    "0.007123823012\n",
    "\n",
    "#### 多进程\n",
    "\n",
    "python 多进程 multiprocessing 的目的是为了提高多核利用率，适用于 cpu 密集的代码。需要注意的两点是，Pytho 的普通变量**不是进程安全**的，考虑同步互斥时，要使用共享变量类型；**协程中可以包含多进程，但是多进程中不能包含协程**，因为多进程中协程会在 yield 处释放 cpu 直接返回，导致该进程无法再恢复。从另一个角度理解，协程本身的特点也是在单进程中实现 cpu 调度。  \n",
    "**1、进程通信、共享变量**  \n",
    "python 多进程提供了基本所有的共享变量类型，常用的包括：共享队列、共享字典、共享列表、简单变量等，因此也提供了锁机制。具体不在这里赘述，相关模块：**from multiprocessing import Process,Manager,Queue**\n",
    "\n",
    "**2、分片与合并**  \n",
    "多进程在优化 cpu 密集的操作时，一般需要将列表、字典等进行分片操作，在多进程里分别处理，再通过共享变量 merge 到一起，达到利用多核的目的，注意根据具体逻辑来判断是否需要加锁。这里的处理其实类似于 golang 中的协程并发，只是它的协程可以分配到多核，同样也需要 channel 来进行通信 。\n",
    "\n",
    "```\n",
    "from multiprocessing import Pool\n",
    "p = Pool(4)\n",
    "\n",
    "for i in range(5):\n",
    "    p.apply\\_async(long\\_time\\_task, args=(i,))\n",
    "p.close()\n",
    "p.join()\n",
    "\n",
    "```\n",
    "\n",
    "#### 多线程\n",
    "\n",
    "Python 多线程一般适用于 IO 密集型的代码，IO 阻塞可以释放 GIL 锁，其他线程可以继续执行，并且线程切换代价要小于进程切换。**要注意的是 python 中 time.sleep() 可以阻塞进程，但不会阻塞线程**。\n",
    "\n",
    "```\n",
    "class ThreadObj():\n",
    "    executor = ThreadPoolExecutor(16)\n",
    "\n",
    "    @run\\_on\\_executor\n",
    "    def function(self):\n",
    "        \n",
    "        time.sleep(5)\n",
    "\n",
    "```\n",
    "\n",
    "#### 协程\n",
    "\n",
    "协程可以简单地理解为一种特殊的程序调用，特殊的是在执行过程中，在子程序内部可中断，然后转而执行别的子程序，在适当的时候再返回来接着执行。如果熟知了 python 生成器，其实可以知道协程也是由生成器实现的，因此也可以将协程理解为**生成器 + 调度策略**。通过调度策略来驱动生成器的执行和调度，达到协程的目的。这里的调度策略可能有很多种，简单的例如忙轮循：while True, 更简单的甚至是一个 for 循环。复杂的可能是基于 epoll 的事件循环。在 python2 的 tornado 中，以及 python3 的 asyncio 中，都对协程的用法做了更好的封装，通过 yield 和 await 就可以使用协程。但其基本实现仍然是这种生成器 + 调度策略的模式。使用协程可以在单线程内实现 cpu 的释放和调度，不再需要进程或线程切换，只是函数调用的消耗。在这里我们举一个简单的生产消费例子：\n",
    "\n",
    "```\n",
    "def consumer():\n",
    "    r = ''\n",
    "    while True:\n",
    "        n = yield r\n",
    "        if not n:\n",
    "            return\n",
    "        print('\\[CONSUMER\\] Consuming %s...' % n)\n",
    "        r = '200 OK'\n",
    "\n",
    "def produce(c):\n",
    "    r=c.send(None)\n",
    "    print r\n",
    "    n = 0\n",
    "    while n<5:\n",
    "        n = n + 1\n",
    "        print('\\[PRODUCER\\] Producing %s...' % n)\n",
    "        r = c.send(n)\n",
    "        print('\\[PRODUCER\\] Consumer return: %s' % r)\n",
    "    c.close()\n",
    "\n",
    "c = consumer()\n",
    "produce(c)\n",
    "\n",
    "```\n",
    "\n",
    "#### 使用合适的 python 解释器\n",
    "\n",
    "CPython：是用 C 语言实现 Pyhon，是目前应用最广泛的解释器。最新的语言特性都是在这个上面先实现，基本包含了所有第三方库支持，但是 CPython 有几个缺陷，一是全局锁使 Python 在多线程效能上表现不佳，二是 CPython 无法支持 JIT（即时编译），导致其执行速度不及 Java 和 Javascipt 等语言。于是出现了 Pypy。\n",
    "\n",
    "Pypy：是用 Python 自身实现的解释器。针对 CPython 的缺点进行了各方面的改良，性能得到很大的提升。最重要的一点就是 Pypy 集成了 JIT。但是，Pypy 无法支持官方的 C/Python API，导致无法使用例如 Numpy，Scipy 等重要的第三方库。这也是现在 Pypy 没有被广泛使用的原因吧。\n",
    "\n",
    "Jython：Jython 是将 python code 在 JVM 上面跑和调用 java code 的解释器。\n",
    "\n",
    "#### 一些建议\n",
    "\n",
    "合理使用 copy 与 deepcopy\n",
    "\n",
    "使用 join 合并迭代器中的字符串\n",
    "\n",
    "使用最佳的反序列化方式 json>cPickle>eval。\n",
    "\n",
    "不借助中间变量交换两个变量的值（有循环引用造成内存泄露的风险）。\n",
    "\n",
    "不局限于 python 内置函数，一些情况下，内置函数的性能，远远不如自己写的。比如 python 的 strptime 方法，会生成一个 9 位的时间元祖，经常需要根据此元祖计算时间戳，该方法性能很差。我们完全可以自己将时间字符串转成 split 成需要的时间元祖。\n",
    "\n",
    "用生成器改写直接返回列表的复杂函数，用列表推导替代简单函数，但是列表推导不要超过两个表达式。生成器 > 列表推导 > map/filter。\n",
    "\n",
    "关键代码可以依赖于高性能的扩展包，因此有时候需要牺牲一些可移植性换取性能； 勇于尝试 python 新版本。\n",
    "\n",
    "考虑优化的成本，一般先从数据结构和算法上优化，改善时间 / 空间复杂度，比如使用集合、分治、贪心、动态规划等，最后再从架构和整体框架上考虑。\n",
    "\n",
    "Python 代码的优化也需要具体问题具体分析，不局限于以上方式，但只要能够分析出性能瓶颈，问题就解决了一半。《约束理论与企业优化》中指出：“**除了瓶颈之外，任何改进都是幻觉”。**\n",
    "\n",
    "### 三、优化实例\n",
    "\n",
    "#### 优化循环\n",
    "\n",
    "将无关代码提到循环上层  \n",
    "![](https://segmentfault.com/img/bVbMxzw)  \n",
    "去掉冗余循环  \n",
    "![](https://segmentfault.com/img/bVbMxzy)  \n",
    "平均耗时由 2.0239s 提升到 0.7896s，性能提升了 61%\n",
    "\n",
    "#### 多进程\n",
    "\n",
    "采用多进程将无关主进程的函数放到后台执行：  \n",
    "![](https://segmentfault.com/img/bVbMxzS)  \n",
    "将列表分片到多进程中执行：  \n",
    "![](https://segmentfault.com/img/bVbMxAt)  \n",
    "如图，1s 内返回的请求比例提升了十个百分点，性能提升 200ms 左右，但不建议代码中过多使用，在业务高峰时会对机器负载造成压力。  \n",
    "![](https://segmentfault.com/img/bVbMxAM)\n",
    "\n",
    "#### 时间元祖代替 strptime 方法\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMxA8)  \n",
    "![](https://segmentfault.com/img/bVbMxBc)  \n",
    "如图，模块平均耗时由 123ms 提升到 79ms，提升 35.7%，并且对一些 badcase 优化效果会更明显：  \n",
    "![](https://segmentfault.com/img/bVbMxBj)![](https://segmentfault.com/img/bVbMxBk)\n",
    "\n",
    "#### 应用集合\n",
    "\n",
    "**将复杂字典转成 md5 的可 hash 的字符串后**，通过集合去重，性能提升 60% 以上。数据量越大，优化效果越好。  \n",
    "![](https://segmentfault.com/img/bVbMxCr)\n",
    "\n",
    "#### 微服务业务解耦\n",
    "\n",
    "将特征计算作为分布式微服务，实现 IO 与计算解耦，将 cpu 密集型转为 IO 密集，在框架和服务选用方面，我们分别测试了 tornado 协程、uwsgi 多进程、import 代码库、celery 分布式计算等多种方式，在性能及可用性上 tornado 都有一定优势，上层 nginx 来代理做端口转发和负载均衡：  \n",
    "ab 压测前后性能对比，虽然在单条请求上并没有优势，但是对高并发系统来说，并发量明显提升：  \n",
    "ab 压测前后性能对比，虽然在单条请求上并没有优势，但是对高并发系统来说，并发量明显提升：  \n",
    "![](https://segmentfault.com/img/bVbMxC6)  \n",
    "![](https://segmentfault.com/img/bVbMxC9)\n",
    "\n",
    "#### 耗时模块 pipeline 实时计算：\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMxDD)\n",
    "\n",
    "![](https://segmentfault.com/img/bVbMxDF)  \n",
    "![](https://segmentfault.com/img/bVbMxDH)\n",
    "\n",
    "命中 pipeline 实时特征后的性能提升：  \n",
    "![](https://segmentfault.com/img/bVbMxDT)\n",
    "\n",
    "python 的高性能服务框架\n",
    "---------------\n",
    "\n",
    "虽然 python 的语言特性导致它在 cpu 密集型的代码中性能堪忧，但是 python 却很适合 IO 密集型的网络应用，加上它优异的数据分析处理能力以及广泛的第三方支持，python 在服务框架上也应用广泛。\n",
    "\n",
    "例如 Django、flask、Tornado，如果考虑性能优先，就要选择高性能的服务框架。Python 的高性能服务基本都是**协程和基于 epoll 的事件循环**实现的 IO 多路复用框架。tornado 依靠强大的 ioloop 事件循环和 gen 封装的协程，让我们可以用 yield 关键字同步式地写出异步代码。\n",
    "\n",
    "在 python3.5 + 中，python 引入原生的异步网络库 asyncio，提供了原生的事件循环 **get\\_event\\_loop** 来支持协程。并用 **async/await** 对协程做了更好的封装。在 tornado6.0 中，ioloop 已经已经实现了对 asyncio 事件循环的封装。除了标准库 asyncio 的事件循环，社区使用 Cython 实现了另外一个事件循环 **uvloop**。用来取代标准库。号称是性能最好的 python 异步 IO 库。之前提到 python 的高性能服务实现都是基于协程和事件循环，**因此我们可以尝试不同的协程和事件循环组合，对 tornado 服务进行改造，实现最优的性能搭配**。  \n",
    "![](https://segmentfault.com/img/bVbMxEo)  \n",
    "篇幅原因这里不详细展开，我们可以简单看下在 python2 和 python3 中异步服务框架的性能表现，发现在服务端的事件循环中，python3 优势明显。而且在三方库的兼容，其他异步性能库的支持上, 以及在协程循环及关键字支持等语法上，还是推荐使用 python3，在更加复杂的项目中，新版的优势会显而易见。但不论新旧版本的 python，协程 + 事件循环的效率都要比多进程或线程高的多。这里顺便贴一个 python3 支持协程的异步 IO 库，基本支持了常见的中间件：[https://github.com/aio-libs?p...](https://github.com/aio-libs?page=1)  \n",
    "![](https://segmentfault.com/img/bVbMxFS)  \n",
    "![](https://segmentfault.com/img/bVbMxF0)  \n",
    "![](https://segmentfault.com/img/bVbMxF1)  \n",
    "![](https://segmentfault.com/img/bVbMxF2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {},
   "toc_section_display": true,
   "toc_window_display": false
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
